import pandas as pd
import numpy as np
df_decathlon= pd.read_csv('src/decathlon.csv', sep=';')
df_decathlon.head()
| Unnamed: 0 | 100m | Longueur | Poids | Hauteur | 400m | 110m H | Disque | Perche | Javelot | 1500m | Classement | Points | Competition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Sebrle | 10.85 | 7.84 | 16.36 | 2.12 | 48.36 | 14.05 | 48.72 | 5.0 | 70.52 | 280.01 | 1 | 8893 | JO |
| 1 | Clay | 10.44 | 7.96 | 15.23 | 2.06 | 49.19 | 14.13 | 50.11 | 4.9 | 69.71 | 282.00 | 2 | 8820 | JO |
| 2 | Karpov | 10.50 | 7.81 | 15.93 | 2.09 | 46.81 | 13.97 | 51.65 | 4.6 | 55.54 | 278.11 | 3 | 8725 | JO |
| 3 | Macey | 10.89 | 7.47 | 15.73 | 2.15 | 48.97 | 14.56 | 48.34 | 4.4 | 58.46 | 265.42 | 4 | 8414 | JO |
| 4 | Warners | 10.62 | 7.74 | 14.48 | 1.97 | 47.97 | 14.01 | 43.73 | 4.9 | 55.39 | 278.05 | 5 | 8343 | JO |
df_decathlon.rename(columns={'Unnamed: 0':'Nom'}, inplace=True)
df_decathlon.head()
| Nom | 100m | Longueur | Poids | Hauteur | 400m | 110m H | Disque | Perche | Javelot | 1500m | Classement | Points | Competition | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | Sebrle | 10.85 | 7.84 | 16.36 | 2.12 | 48.36 | 14.05 | 48.72 | 5.0 | 70.52 | 280.01 | 1 | 8893 | JO |
| 1 | Clay | 10.44 | 7.96 | 15.23 | 2.06 | 49.19 | 14.13 | 50.11 | 4.9 | 69.71 | 282.00 | 2 | 8820 | JO |
| 2 | Karpov | 10.50 | 7.81 | 15.93 | 2.09 | 46.81 | 13.97 | 51.65 | 4.6 | 55.54 | 278.11 | 3 | 8725 | JO |
| 3 | Macey | 10.89 | 7.47 | 15.73 | 2.15 | 48.97 | 14.56 | 48.34 | 4.4 | 58.46 | 265.42 | 4 | 8414 | JO |
| 4 | Warners | 10.62 | 7.74 | 14.48 | 1.97 | 47.97 | 14.01 | 43.73 | 4.9 | 55.39 | 278.05 | 5 | 8343 | JO |
df_decathlon.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 41 entries, 0 to 40 Data columns (total 14 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 Nom 41 non-null object 1 100m 41 non-null float64 2 Longueur 41 non-null float64 3 Poids 41 non-null float64 4 Hauteur 41 non-null float64 5 400m 41 non-null float64 6 110m H 41 non-null float64 7 Disque 41 non-null float64 8 Perche 41 non-null float64 9 Javelot 41 non-null float64 10 1500m 41 non-null float64 11 Classement 41 non-null int64 12 Points 41 non-null int64 13 Competition 41 non-null object dtypes: float64(10), int64(2), object(2) memory usage: 4.6+ KB
Pas de données manquantes
df_decathlon.describe()
| 100m | Longueur | Poids | Hauteur | 400m | 110m H | Disque | Perche | Javelot | 1500m | Classement | Points | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| count | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 | 41.000000 |
| mean | 10.998049 | 7.260000 | 14.477073 | 1.976829 | 49.616341 | 14.605854 | 44.325610 | 4.762439 | 58.316585 | 279.024878 | 12.121951 | 8005.365854 |
| std | 0.263023 | 0.316402 | 0.824428 | 0.088951 | 1.153451 | 0.471789 | 3.377845 | 0.278000 | 4.826820 | 11.673247 | 7.918949 | 342.385145 |
| min | 10.440000 | 6.610000 | 12.680000 | 1.850000 | 46.810000 | 13.970000 | 37.920000 | 4.200000 | 50.310000 | 262.100000 | 1.000000 | 7313.000000 |
| 25% | 10.850000 | 7.030000 | 13.880000 | 1.920000 | 48.930000 | 14.210000 | 41.900000 | 4.500000 | 55.270000 | 271.020000 | 6.000000 | 7802.000000 |
| 50% | 10.980000 | 7.300000 | 14.570000 | 1.950000 | 49.400000 | 14.480000 | 44.410000 | 4.800000 | 58.360000 | 278.050000 | 11.000000 | 8021.000000 |
| 75% | 11.140000 | 7.480000 | 14.970000 | 2.040000 | 50.300000 | 14.980000 | 46.070000 | 4.920000 | 60.890000 | 285.100000 | 18.000000 | 8122.000000 |
| max | 11.640000 | 7.960000 | 16.360000 | 2.150000 | 53.200000 | 15.670000 | 51.650000 | 5.400000 | 70.520000 | 317.000000 | 28.000000 | 8893.000000 |
Pas de données aberrantes à première vue.
corr_matrice = df_decathlon.corr(numeric_only=True)
corr_matrice
| 100m | Longueur | Poids | Hauteur | 400m | 110m H | Disque | Perche | Javelot | 1500m | Classement | Points | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 100m | 1.000000 | -0.598678 | -0.356482 | -0.246253 | 0.520298 | 0.579889 | -0.221708 | -0.082537 | -0.157746 | -0.060546 | 0.296704 | -0.684272 |
| Longueur | -0.598678 | 1.000000 | 0.183304 | 0.294644 | -0.602063 | -0.505410 | 0.194310 | 0.204014 | 0.119759 | -0.033686 | -0.604055 | 0.725135 |
| Poids | -0.356482 | 0.183304 | 1.000000 | 0.489212 | -0.138433 | -0.251616 | 0.615768 | 0.061182 | 0.374956 | 0.115803 | -0.369970 | 0.627389 |
| Hauteur | -0.246253 | 0.294644 | 0.489212 | 1.000000 | -0.187957 | -0.283289 | 0.369218 | -0.156181 | 0.171880 | -0.044903 | -0.492769 | 0.576703 |
| 400m | 0.520298 | -0.602063 | -0.138433 | -0.187957 | 1.000000 | 0.547988 | -0.117879 | -0.079292 | 0.004232 | 0.408106 | 0.562119 | -0.666940 |
| 110m H | 0.579889 | -0.505410 | -0.251616 | -0.283289 | 0.547988 | 1.000000 | -0.326201 | -0.002704 | 0.008743 | 0.037540 | 0.439102 | -0.644460 |
| Disque | -0.221708 | 0.194310 | 0.615768 | 0.369218 | -0.117879 | -0.326201 | 1.000000 | -0.150072 | 0.157890 | 0.258175 | -0.389125 | 0.484183 |
| Perche | -0.082537 | 0.204014 | 0.061182 | -0.156181 | -0.079292 | -0.002704 | -0.150072 | 1.000000 | -0.030001 | 0.247448 | -0.320380 | 0.197436 |
| Javelot | -0.157746 | 0.119759 | 0.374956 | 0.171880 | 0.004232 | 0.008743 | 0.157890 | -0.030001 | 1.000000 | -0.180393 | -0.208095 | 0.422393 |
| 1500m | -0.060546 | -0.033686 | 0.115803 | -0.044903 | 0.408106 | 0.037540 | 0.258175 | 0.247448 | -0.180393 | 1.000000 | 0.089898 | -0.194349 |
| Classement | 0.296704 | -0.604055 | -0.369970 | -0.492769 | 0.562119 | 0.439102 | -0.389125 | -0.320380 | -0.208095 | 0.089898 | 1.000000 | -0.739183 |
| Points | -0.684272 | 0.725135 | 0.627389 | 0.576703 | -0.666940 | -0.644460 | 0.484183 | 0.197436 | 0.422393 | -0.194349 | -0.739183 | 1.000000 |
import matplotlib.pyplot as plt
import seaborn as sns
# Changement de police des graphiques
plt.rc('font', family = 'serif', serif = 'cmr10')
plt.rcParams.update({"text.usetex": True, "axes.formatter.use_mathtext" : True})
sns.heatmap(corr_matrice, annot=True, fmt=".1f", cmap='RdBu', vmin=-1, vmax=1, center=0, linewidth=0.5)
plt.title('Corrélations entre les performances dans les différentes disciplines')
plt.show()
Compte-tenu du nombre de variables, il est difficile de donner une interprétation globale de ce diagramme. On peut tout de même remarquer :
from sklearn.preprocessing import StandardScaler
from sklearn.decomposition import PCA
from sklearn.pipeline import make_pipeline
scaler = StandardScaler()
pca = PCA()
pipe = make_pipeline(scaler, pca)
features = df_decathlon.iloc[:,1:11]
features_trans = pipe.fit_transform(features)
features_trans = pd.DataFrame(features_trans, columns=['PC'+str(i) for i in range(1,11)])
features_trans.head()
| PC1 | PC2 | PC3 | PC4 | PC5 | PC6 | PC7 | PC8 | PC9 | PC10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 4.038449 | 1.365826 | -0.289957 | 1.941134 | -0.376955 | 0.067786 | -0.554977 | -0.752596 | 0.062225 | 0.633131 |
| 1 | 3.919365 | 0.836961 | 0.231175 | 1.493972 | 1.037609 | -0.812650 | -0.867515 | -0.302845 | -0.013215 | -0.818729 |
| 2 | 4.619987 | 0.039995 | -0.041586 | -1.313526 | -0.187730 | 0.741611 | -0.454143 | 1.070842 | -0.180315 | 0.124575 |
| 3 | 2.233461 | 1.041766 | -1.864362 | -0.743214 | -0.977270 | -0.040017 | -0.192708 | 0.689743 | 0.438425 | -0.166834 |
| 4 | 2.168396 | -1.803200 | 0.851017 | -0.284600 | 0.151395 | -0.079388 | 0.061003 | 0.214545 | 0.167392 | 0.082692 |
caract_pc = pd.DataFrame({'Composante':['PC'+str(i) for i in range(1,11)], 'Valeur propre' : np.round(pca.explained_variance_,2),
'% Variance' : np.round(pca.explained_variance_ratio_*100,2), '% Variance cumulée' : np.round(np.cumsum(pca.explained_variance_ratio_*100),2)})
caract_pc.set_index('Composante')
| Valeur propre | % Variance | % Variance cumulée | |
|---|---|---|---|
| Composante | |||
| PC1 | 3.35 | 32.72 | 32.72 |
| PC2 | 1.78 | 17.37 | 50.09 |
| PC3 | 1.44 | 14.05 | 64.14 |
| PC4 | 1.08 | 10.57 | 74.71 |
| PC5 | 0.70 | 6.85 | 81.56 |
| PC6 | 0.61 | 5.99 | 87.55 |
| PC7 | 0.46 | 4.51 | 92.06 |
| PC8 | 0.41 | 3.97 | 96.03 |
| PC9 | 0.22 | 2.15 | 98.18 |
| PC10 | 0.19 | 1.82 | 100.00 |
fig, ax = plt.subplots(figsize=(5,5))
ax.bar(np.arange(1,11,1), caract_pc['% Variance'], color='tab:blue')
ax.set_xticks(np.arange(1,11,1), caract_pc['Composante'])
ax.tick_params(axis='y', color='tab:blue', labelcolor='tab:blue')
plt.title("Pourcentage de variance expliquée par chaque composante principale")
ax.set_xlabel("Composante principale")
ax.set_ylabel("Pourcentage de variance expliquée", color='tab:blue')
#Deuxième axe pour les pourcentages cumulés
ax2 = ax.twinx()
ax2.set_ylabel('Pourcentages cumulés', color='tab:red')
ax2.tick_params(axis='y', color='tab:red', labelcolor='tab:red')
ax2.set_ylim(0,100)
ax2.plot(np.arange(1,11,1), caract_pc['% Variance cumulée'], color='tab:red', marker='x')
plt.show()
Il n'y a pas de coude "franc" visuellement parlant. On constate un grand écart de variance expliquée entre 1 et 2 composantes principales, mais ne retenir que deux composantes ne permet d'expliquer que 50 % de la variance.
On constate un autre écart entre 4 et 5 composantes mais moins important. Pour une utilisation ultérieure, il semble que 5 composantes principales soit un bon choix. Cela permet une explication de 82 % de la variance.
bb = 1/np.arange(10,0,-1) #Calcul des fraction 1/k pour k allant de 10 à 1 (ordre descendant)
bb = np.cumsum(bb) #calcul des sommes cumulées des 1/k
bb = bb[::-1] #inversion de la liste des valeurs pour obtenir les bâtons brisés dans le bon ordre
bb
array([2.92896825, 1.92896825, 1.42896825, 1.09563492, 0.84563492,
0.64563492, 0.47896825, 0.33611111, 0.21111111, 0.1 ])
test_bb = pd.DataFrame({'PC':np.arange(1,11), 'Val propre' : pca.explained_variance_, 'batons':bb})
test_bb
| PC | Val propre | batons | |
|---|---|---|---|
| 0 | 1 | 3.353703 | 2.928968 |
| 1 | 2 | 1.780559 | 1.928968 |
| 2 | 3 | 1.440040 | 1.428968 |
| 3 | 4 | 1.083272 | 1.095635 |
| 4 | 5 | 0.701893 | 0.845635 |
| 5 | 6 | 0.614250 | 0.645635 |
| 6 | 7 | 0.462516 | 0.478968 |
| 7 | 8 | 0.406799 | 0.336111 |
| 8 | 9 | 0.220185 | 0.211111 |
| 9 | 10 | 0.186783 | 0.100000 |
fig, ax = plt.subplots(figsize=(5,5))
plt.plot(test_bb['PC'], test_bb['Val propre'], label="Valeur propre", color='tab:blue')
plt.plot(test_bb['PC'], test_bb['batons'], color='tab:red', label="Seuil des bâtons brisés", linestyle="--")
plt.xticks(test_bb['PC'], ['PC'+str(i) for i in range(1,11)])
plt.title("Test des bâtons brisés")
plt.xlabel("Composante principale")
plt.ylabel("Valeur propre")
plt.legend(bbox_to_anchor=(1,1))
plt.show()
Le test suggère de ne garder que la première composante principale (la valeur prore de la deuxième composante est inférieure au seuil pour cette composante), qui ne représente que 33% de la variance. Cela paraît peu. De plus, la courbe des seuils de bâtons brisés suit approximativement la courbe des valeurs propres (qui repasse même au-dessus à partir de PC8).
L'ACP a été réalisée sur des valeurs normalisées. La somme des valeurs propres est égale au nombre de variables et leurs moyenne est 1. Le seuil de Kaiser-Guttman est donc 1.
fig, ax = plt.subplots(figsize=(5,5))
ax.bar(np.arange(1,11,1), caract_pc['Valeur propre'], color='tab:blue')
ax.axhline(y=1, xmin=0, xmax=10, linestyle='--', color='crimson')
ax.text(6,1.1,'Seuil de Kaiser-guttman', color='crimson')
ax.set_xticks(np.arange(1,11,1), caract_pc['Composante'])
plt.title("Variance sur chaque composante principale")
ax.set_xlabel("Composante principale")
ax.set_ylabel("Variance")
plt.show()
Cette règle suggère de garder 4 composantes principales.
La règle de Kaiser-Guttman ne tient pas compte des dimensions des données. Elle peut être trop permissive dans certains cas. La règle de Karlis-Saporta-Spinaki surélève le seuil de Kaiser-Guttman pour tenir compte de la dimension des données :
$Seuil = 1+2\sqrt{\dfrac{p-1}{n-1}}$ où $p$ est le nombre de variables et $n$ le nombre d'observations.
Ici, Le seuil vaut : $1+2\times \sqrt{\dfrac{9}{40}}\approx 1,95$.
fig, ax = plt.subplots(figsize=(5,5))
ax.bar(np.arange(1,11,1), caract_pc['Valeur propre'], color='tab:blue')
ax.axhline(y=1.95, xmin=0, xmax=10, linestyle='--', color='crimson')
ax.text(5,2,'Seuil de Karlis-Saporta-Spinaki', color='crimson')
ax.set_xticks(np.arange(1,11,1), caract_pc['Composante'])
plt.title("Variance sur chaque composante principale")
ax.set_xlabel("Composante principale")
ax.set_ylabel("Variance")
plt.show()
Cette règle suggère de ne garder qu'une composante principale, comme le test des bâtons brisés.
# Calcul des saturations des variables sur les PC
saturations = pd.DataFrame(pca.components_.T*np.sqrt(pca.explained_variance_),
columns=['PC'+str(i) for i in range(1,11)],
index=df_decathlon.columns[1:11])
saturations
| PC1 | PC2 | PC3 | PC4 | PC5 | PC6 | PC7 | PC8 | PC9 | PC10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 100m | -0.784344 | 0.189467 | -0.186698 | -0.038288 | -0.305951 | 0.232048 | -0.259640 | -0.294413 | 0.049156 | 0.183368 |
| Longueur | 0.751116 | -0.349712 | 0.184475 | 0.103050 | -0.037134 | -0.239923 | -0.426885 | 0.013401 | 0.226487 | 0.035026 |
| Poids | 0.630236 | 0.605736 | -0.023669 | 0.192959 | -0.112532 | 0.239412 | 0.210640 | 0.200227 | 0.200501 | 0.168675 |
| Hauteur | 0.579050 | 0.354645 | -0.262736 | -0.137279 | -0.562340 | -0.366612 | 0.062194 | -0.079784 | -0.114335 | -0.045996 |
| 400m | -0.688053 | 0.576512 | 0.133103 | 0.029666 | 0.088781 | -0.260611 | 0.084617 | -0.136107 | 0.259081 | -0.178922 |
| 110m H | -0.755516 | 0.231636 | -0.093788 | 0.294444 | -0.166362 | -0.078090 | -0.243015 | 0.453554 | -0.070449 | -0.039270 |
| Disque | 0.559330 | 0.613846 | 0.043486 | -0.262897 | 0.106129 | 0.352192 | -0.292362 | -0.024485 | -0.072642 | -0.194122 |
| Perche | 0.050967 | -0.182597 | 0.700350 | 0.558386 | -0.334058 | 0.205077 | 0.066621 | -0.113554 | -0.038936 | -0.119478 |
| Javelot | 0.280553 | 0.320927 | -0.394496 | 0.721126 | 0.308919 | -0.127909 | -0.072596 | -0.188882 | -0.116055 | 0.037934 |
| 1500m | -0.058799 | 0.480115 | 0.791859 | -0.163090 | 0.155470 | -0.233766 | -0.056875 | -0.008749 | -0.144401 | 0.185507 |
# saturations de classement et points sur les composantes PC1 et PC2
scaler = StandardScaler()
resultats_stand = pd.DataFrame(scaler.fit_transform(df_decathlon[['Classement','Points']]), columns=['Classement','Points'])
sat_class_pc1 = np.corrcoef(features_trans.iloc[:,0], df_decathlon['Classement'])[0,1]
sat_class_pc2 = np.corrcoef(features_trans.iloc[:,1], df_decathlon['Classement'])[0,1]
sat_class_pc3 = np.corrcoef(features_trans.iloc[:,2], df_decathlon['Classement'])[0,1]
sat_class_pc4 = np.corrcoef(features_trans.iloc[:,3], df_decathlon['Classement'])[0,1]
sat_pts_pc1 = np.corrcoef(features_trans.iloc[:,0], df_decathlon['Points'])[0,1]
sat_pts_pc2 = np.corrcoef(features_trans.iloc[:,1], df_decathlon['Points'])[0,1]
sat_pts_pc3 = np.corrcoef(features_trans.iloc[:,2], df_decathlon['Points'])[0,1]
sat_pts_pc4 = np.corrcoef(features_trans.iloc[:,3], df_decathlon['Points'])[0,1]
# Calcul des cos2
cos2 = saturations**2
cos2
| PC1 | PC2 | PC3 | PC4 | PC5 | PC6 | PC7 | PC8 | PC9 | PC10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 100m | 0.615196 | 0.035898 | 0.034856 | 0.001466 | 0.093606 | 0.053846 | 0.067413 | 0.086679 | 0.002416 | 0.033624 |
| Longueur | 0.564176 | 0.122299 | 0.034031 | 0.010619 | 0.001379 | 0.057563 | 0.182231 | 0.000180 | 0.051296 | 0.001227 |
| Poids | 0.397197 | 0.366916 | 0.000560 | 0.037233 | 0.012663 | 0.057318 | 0.044369 | 0.040091 | 0.040201 | 0.028451 |
| Hauteur | 0.335299 | 0.125773 | 0.069030 | 0.018845 | 0.316226 | 0.134404 | 0.003868 | 0.006366 | 0.013072 | 0.002116 |
| 400m | 0.473416 | 0.332366 | 0.017716 | 0.000880 | 0.007882 | 0.067918 | 0.007160 | 0.018525 | 0.067123 | 0.032013 |
| 110m H | 0.570804 | 0.053655 | 0.008796 | 0.086697 | 0.027676 | 0.006098 | 0.059056 | 0.205711 | 0.004963 | 0.001542 |
| Disque | 0.312850 | 0.376806 | 0.001891 | 0.069115 | 0.011263 | 0.124039 | 0.085475 | 0.000600 | 0.005277 | 0.037683 |
| Perche | 0.002598 | 0.033342 | 0.490490 | 0.311794 | 0.111595 | 0.042057 | 0.004438 | 0.012895 | 0.001516 | 0.014275 |
| Javelot | 0.078710 | 0.102994 | 0.155627 | 0.520022 | 0.095431 | 0.016361 | 0.005270 | 0.035676 | 0.013469 | 0.001439 |
| 1500m | 0.003457 | 0.230510 | 0.627041 | 0.026598 | 0.024171 | 0.054646 | 0.003235 | 0.000077 | 0.020852 | 0.034413 |
cos2_pc12 = cos2['PC1']+cos2['PC2']
cos2_pc12
100m 0.651093 Longueur 0.686474 Poids 0.764113 Hauteur 0.461073 400m 0.805782 110m H 0.624459 Disque 0.689656 Perche 0.035939 Javelot 0.181704 1500m 0.233968 dtype: float64
#Echelle de couleurs pour représenter les cos2
import cmasher as cmr
import matplotlib.colors as mcolors
cmap = cmr.get_sub_cmap('cmr.ember', 0.1, 0.9)
norm = mcolors.Normalize(vmin=0, vmax=1)
color=[]
for i in range(len(cos2_pc12)):
color.append(cmap(norm(cos2_pc12[i])))
df_cos2=pd.DataFrame({'cos2':cos2_pc12, 'couleur':color})
df_cos2
| cos2 | couleur | |
|---|---|---|
| 100m | 0.651093 | (0.8574156, 0.25216222, 0.09236499, 1.0) |
| Longueur | 0.686474 | (0.87617742, 0.29951158, 0.06895674, 1.0) |
| Poids | 0.764113 | (0.91066237, 0.40470882, 0.01887928, 1.0) |
| Hauteur | 0.461073 | (0.67755703, 0.037385, 0.23467744, 1.0) |
| 400m | 0.805782 | (0.92411787, 0.45632484, 0.00609634, 1.0) |
| 110m H | 0.624459 | (0.83912274, 0.21082034, 0.11285311, 1.0) |
| Disque | 0.689656 | (0.87865734, 0.30619915, 0.06561995, 1.0) |
| Perche | 0.035939 | (0.11611032, 0.06513579, 0.13799796, 1.0) |
| Javelot | 0.181704 | (0.29929909, 0.10148169, 0.23800843, 1.0) |
| 1500m | 0.233968 | (0.37076004, 0.10304797, 0.2591165, 1.0) |
# Représentation par un cercle de corrélation
fig, ax = plt.subplots(figsize=(7,7))
ax.set_aspect('equal')
# Tracer les axes
plt.axhline(y=0, color='black', linewidth=1, linestyle='--')
plt.axvline(x=0, color='black', linewidth=1, linestyle='--')
# Tracer le cercle de rayon 1
circle = plt.Circle((0,0),1, edgecolor='black', facecolor='white')
ax.add_artist(circle)
# Représentation des vecteurs
for i in range(saturations.shape[0]):
x = saturations.iloc[i,0] # coord de la var i selon PC1
y = saturations.iloc[i,1] # coord de la var i selon PC2
feature = saturations.index[i] # nom de la var i
ax.arrow(0,0, #départ flèche
x,y, #fin flèche
head_width=0.05,
head_length=0.05,
color=df_cos2.iloc[i,1],
length_includes_head=True
)
if feature!='Poids':
if x>0:
ax.text(x+0.05, y+0.05, feature)
else:
ax.text(x-0.15, y+0.05, feature)
else :
ax.text(x+0.05,y-0.05, feature)
# Vecteur de classement
ax.arrow(0,0, #départ flèche
sat_class_pc1, sat_class_pc2, #fin flèche
head_width=0.05,
head_length=0.05,
color='tab:blue',
length_includes_head=True
)
ax.text(sat_class_pc1-0.35, sat_class_pc2, 'Classement', color='tab:blue')
# Vecteur de points
ax.arrow(0,0, #départ flèche
sat_pts_pc1, sat_pts_pc2, #fin flèche
head_width=0.05,
head_length=0.05,
color='tab:blue',
length_includes_head=True
)
ax.text(sat_pts_pc1-0.2, sat_pts_pc2-0.08, 'Points', color='tab:blue')
#legende des couleurs
sm = plt.cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin=0, vmax=1)
plt.colorbar(sm, label="cos2", shrink=0.4,orientation='vertical',pad=0.1, ax=ax)
# labels et titres
plt.xlim(-1.2,1.2)
plt.ylim(-1.2,1.2)
plt.xticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.yticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
plt.ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' \%)')
plt.title('Cercle de corrélation des variables du jeu de données Decathlon')
plt.tight_layout()
plt.show()
Les variables les mieux représentées sur le plan factoriel (PC1, PC2) sont le 400m, le lancer du poids, le lancer du disque et le saut en longueur.
Appelons groupe 1 les disciplines suivantes : disque, poids. Appelons groupe 2 les disciplines suivantes : 400m, 100m, 110m haies, longueur.
Le nombre de points est corrélé positivement (mais pas très fortement) avec les performances au lancer du disque, au lancer du poids, au saut en longueur et au saut en hauteur.
Certaines variables comme le javelot, le saut à la perche et le 1500m ne sont pas bien expliquées par le plan factoriel (PC1, PC2). D'après la table des cos2, le javelot et le saut à la perche sont mieux expliquées par le plan
(PC3, PC4) alors que le 1500m est mieux expliqué par le plan (PC2, PC3). Regardons le cercle de corrélation du plan (PC3, PC4).
# Calcul des cos2
cos2_pc34 = cos2['PC3']+cos2['PC4']
cos2_pc34
100m 0.036322 Longueur 0.044650 Poids 0.037794 Hauteur 0.087876 400m 0.018596 110m H 0.095493 Disque 0.071006 Perche 0.802285 Javelot 0.675650 1500m 0.653639 dtype: float64
#Echelle de couleurs pour représenter les cos2
cmap = cmr.get_sub_cmap('cmr.ember', 0.1, 0.9)
norm = mcolors.Normalize(vmin=0, vmax=1)
color=[]
for i in range(len(cos2_pc34)):
color.append(cmap(norm(cos2_pc34[i])))
df_cos2_34=pd.DataFrame({'cos2':cos2_pc34, 'couleur':color})
df_cos2_34
| cos2 | couleur | |
|---|---|---|
| 100m | 0.036322 | (0.11611032, 0.06513579, 0.13799796, 1.0) |
| Longueur | 0.044650 | (0.12770344, 0.06903174, 0.14661238, 1.0) |
| Poids | 0.037794 | (0.11611032, 0.06513579, 0.13799796, 1.0) |
| Hauteur | 0.087876 | (0.18105903, 0.08385552, 0.18180624, 1.0) |
| 400m | 0.018596 | (0.09324732, 0.05663497, 0.11982592, 1.0) |
| 110m H | 0.095493 | (0.18709992, 0.08524014, 0.18537066, 1.0) |
| Disque | 0.071006 | (0.15711534, 0.07779942, 0.16686569, 1.0) |
| Perche | 0.802285 | (0.92411787, 0.45632484, 0.00609634, 1.0) |
| Javelot | 0.675650 | (0.87107311, 0.286081, 0.07562583, 1.0) |
| 1500m | 0.653639 | (0.8574156, 0.25216222, 0.09236499, 1.0) |
# Représentation par un cercle de corrélation
fig, ax = plt.subplots(figsize=(7,7))
ax.set_aspect('equal')
# Tracer les axes
plt.axhline(y=0, color='black', linewidth=1, linestyle='--')
plt.axvline(x=0, color='black', linewidth=1, linestyle='--')
# Tracer le cercle de rayon 1
circle = plt.Circle((0,0),1, edgecolor='black', facecolor='white')
ax.add_artist(circle)
# Représentation des vecteurs
for i in range(saturations.shape[0]):
x = saturations.iloc[i,2] # coord de la var i selon PC3
y = saturations.iloc[i,3] # coord de la var i selon PC4
feature = saturations.index[i] # nom de la var i
ax.arrow(0,0, #départ flèche
x,y, #fin flèche
head_width=0.05,
head_length=0.05,
color=df_cos2_34.iloc[i,1],
length_includes_head=True
)
if feature!='':
if x>0:
plt.text(x+0.03, y+0.03, feature)
else:
plt.text(x-0.2, y, feature)
else :
plt.text(x+0.05,y-0.05, feature)
# Vecteur de classement
ax.arrow(0,0, #départ flèche
sat_class_pc3, sat_class_pc4, #fin flèche
head_width=0.05,
head_length=0.05,
color='tab:blue',
length_includes_head=True
)
ax.text(sat_class_pc3-0.1, sat_class_pc4+0.06, 'Classement', color='tab:blue')
# Vecteur de points
ax.arrow(0,0, #départ flèche
sat_pts_pc3, sat_pts_pc4, #fin flèche
head_width=0.05,
head_length=0.05,
color='tab:blue',
length_includes_head=True
)
ax.text(sat_pts_pc3-0.2, sat_pts_pc4-0.08, 'Points', color='tab:blue')
#legende des couleurs
sm = plt.cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin=0, vmax=1)
plt.colorbar(sm, label="cos2", shrink=0.4,orientation='vertical',pad=0.1, ax=ax)
# labels et titres
plt.xlim(-1.2,1.2)
plt.ylim(-1.2,1.2)
plt.xticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.yticks([i for i in np.arange(-1,1.5,0.5)], [str(i) for i in np.arange(-1,1.5,0.5)])
plt.xlabel('PC3 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
plt.ylabel('PC4 ('+ str(round(pca.explained_variance_ratio_[3]*100,2)) + ' \%)')
plt.title('Cercle de corrélation des variables du jeu de données Decathlon')
plt.tight_layout()
plt.show()
Dans ce plan, les variables Perche et javelot sont bien mieux représentées. Le 1500 m également puisque cette variable est expliquée en bonne partie par PC3.
En revanche les autres disciplines sont très mal représentées sur ce plan.
Finalement, la seule chose qu'apporte ce plan est que les performances au saut à la perche ne sont quasiment pas corrélées aux performances au javelot.
cos2_indiv = ((features_trans**2)
.divide(((features_trans**2)
.sum(axis=1)),axis=0)
)
cos2_indiv.head()
| PC1 | PC2 | PC3 | PC4 | PC5 | PC6 | PC7 | PC8 | PC9 | PC10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 0.695410 | 0.079543 | 0.003585 | 0.160666 | 0.006059 | 0.000196 | 0.013133 | 0.024151 | 0.000165 | 0.017092 |
| 1 | 0.711205 | 0.032432 | 0.002474 | 0.103335 | 0.049846 | 0.030575 | 0.034843 | 0.004246 | 0.000008 | 0.031034 |
| 2 | 0.851755 | 0.000064 | 0.000069 | 0.068851 | 0.001406 | 0.021948 | 0.008230 | 0.045760 | 0.001297 | 0.000619 |
| 3 | 0.423049 | 0.092039 | 0.294777 | 0.046845 | 0.080996 | 0.000136 | 0.003149 | 0.040347 | 0.016301 | 0.002360 |
| 4 | 0.529944 | 0.366472 | 0.081626 | 0.009129 | 0.002583 | 0.000710 | 0.000419 | 0.005188 | 0.003158 | 0.000771 |
cos2_indiv_12 = cos2_indiv['PC1'] + cos2_indiv['PC2']
import seaborn as sns
import cmasher as cmr
# Représentation des individus suivant les deux premières composantes
fig, ax = plt.subplots(figsize=(6,6))
plt.axhline(y=0, color='black', linewidth=1)
plt.axvline(x=0, color='black', linewidth=1)
sns.scatterplot(x='PC1', y='PC2', data=features_trans, size=cos2_indiv_12)
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' \%)')
plt.xlim(-5,5)
plt.ylim(-5,5)
#deuxième repère pour les vecteurs
ax2 = fig.add_axes(ax.get_position(), frame_on=False)
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.xaxis.set_label_position('top')
ax2.yaxis.set_label_position('right')
ax2.set_xlabel('Variables initiales selon PC1', color='chocolate')
ax2.set_ylabel('Variables initiales selon PC2', color='chocolate')
ax2.tick_params(axis='both', labelcolor='chocolate')
ax2.set_xlim(-1.1, 1.1)
ax2.set_ylim(-1.1, 1.1)
ax2.set_xticks([-1, -0.5, 0, 0.5, 1])
ax2.set_yticks([-1, -0.5, 0, 0.5, 1])
# Représentation des vecteurs
for i in range(saturations.shape[0]):
x = saturations.iloc[i,0] # coord de la var i selon PC1
y = saturations.iloc[i,1] # coord de la var i selon PC2
feature = saturations.index[i] # nom de la var i
ax2.arrow(0,0, #départ flèche
x,y, #fin flèche
head_width=0.025,
head_length=0.025,
length_includes_head=True,
color='chocolate'
)
if feature!='Poids':
if x>0:
plt.text(x+0.05, y+0.05, feature)
else:
plt.text(x-0.15, y+0.05, feature)
else :
plt.text(x+0.05,y-0.05, feature)
# Titre
plt.title("Biplot : athlètes et disciplines suivant les deux premières composantes de l'ACP")
ax.legend(title='Cos2 des individus', bbox_to_anchor=[1.4,1])
# Grille
ax.grid(visible=True)
ax.set_axisbelow(True) # grille en arrière-plan
plt.show()
Les individus les mieux représentés sont les plus éloignés de l'origine.
cos2_indiv_123 = cos2_indiv['PC1'] + cos2_indiv['PC2'] + cos2_indiv['PC3']
#Echelle de couleurs pour représenter les cos2
cmap = cmr.get_sub_cmap('cmr.ember', 0.1, 0.9)
norm = mcolors.Normalize(vmin=0, vmax=1)
color=[]
for i in range(len(cos2_indiv_123)):
color.append(cmap(norm(cos2_indiv_123[i])))
df_cos2_123=pd.DataFrame({'cos2':cos2_indiv_123, 'couleur':color})
df_cos2_123.head()
| cos2 | couleur | |
|---|---|---|
| 0 | 0.778538 | (0.91598827, 0.42411283, 0.01281748, 1.0) |
| 1 | 0.746111 | (0.90302556, 0.3787143, 0.02922685, 1.0) |
| 2 | 0.851888 | (0.93763131, 0.5204957, 0.00678164, 1.0) |
| 3 | 0.809866 | (0.92563324, 0.46275282, 0.00528364, 1.0) |
| 4 | 0.978041 | (0.95551873, 0.68816819, 0.10107749, 1.0) |
# Création de la figure
fig = plt.figure(figsize = (30, 10))
ax = plt.axes(projection ="3d")
# Création du graphique
sctt = ax.scatter3D(features_trans['PC1'], features_trans['PC2'], features_trans['PC3'], c=df_cos2_123['couleur'])
# Titres et labels
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' \%)')
ax.set_zlabel('PC3 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
ax.set_title("Répartition des décathlètes suivant les trois premières composantes principales")
#legende des couleurs
sm = plt.cm.ScalarMappable(cmap=cmap)
sm.set_clim(vmin=0, vmax=1)
plt.colorbar(sm, label="Cos2", shrink=0.3, orientation='vertical',pad=0.1, anchor = (0.5,0.5), ax=ax)
# Ajustement de l'affichage
ax.tick_params(axis='x', pad=-0.5)
ax.tick_params(axis='y', pad=-0.5)
ax.tick_params(axis='z', pad=-0.5)
plt.subplots_adjust(left=0.6, right=0.77, top=1, bottom=0.2)
plt.show()
Difficile de distinguer la forme du nuage de points. Utilisons une version interactive.
import plotly.express as px
# Création du graphe
fig = px.scatter_3d(features_trans, x='PC1', y='PC2', z='PC3',
color=cos2_indiv_123, color_continuous_scale=['black', 'firebrick', 'orange'])
# Changement fenêtre de survol
fig.update_traces(hovertemplate="PC1: %{x}<br>PC2: %{y}<br>PC3: %{z}<br>Cos2: %{customdata[0]}",
customdata=np.round(df_cos2_123[['cos2']].values,2))
# Ajout d'une bordure aux points
fig.update_traces(marker=dict(
size=4,
line=dict(width=1,
color='DarkSlateGrey')
)
)
# titres, labels et vue d'origine
fig.update_layout(width=1000,
height=800,
scene = dict(
xaxis_title='PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' %)',
yaxis_title='PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' %)',
zaxis_title='PC3 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' %)'),
title_text="Répartition des décathlètes suivant les trois premières composantes principales",
title_y=0.9,
scene_camera=dict(
eye=dict(x=1.8, y=-1.25, z=0.8)),
autosize=False,
coloraxis_colorbar_title ="Cos2",
)
#Affichage
fig.show()
Calcul des contributions de chaque variable à chaque composante.
contributions = round(cos2/(cos2.sum(axis=0))*100,2)
contributions
| PC1 | PC2 | PC3 | PC4 | PC5 | PC6 | PC7 | PC8 | PC9 | PC10 | |
|---|---|---|---|---|---|---|---|---|---|---|
| 100m | 18.34 | 2.02 | 2.42 | 0.14 | 13.34 | 8.77 | 14.58 | 21.31 | 1.10 | 18.00 |
| Longueur | 16.82 | 6.87 | 2.36 | 0.98 | 0.20 | 9.37 | 39.40 | 0.04 | 23.30 | 0.66 |
| Poids | 11.84 | 20.61 | 0.04 | 3.44 | 1.80 | 9.33 | 9.59 | 9.86 | 18.26 | 15.23 |
| Hauteur | 10.00 | 7.06 | 4.79 | 1.74 | 45.05 | 21.88 | 0.84 | 1.56 | 5.94 | 1.13 |
| 400m | 14.12 | 18.67 | 1.23 | 0.08 | 1.12 | 11.06 | 1.55 | 4.55 | 30.48 | 17.14 |
| 110m H | 17.02 | 3.01 | 0.61 | 8.00 | 3.94 | 0.99 | 12.77 | 50.57 | 2.25 | 0.83 |
| Disque | 9.33 | 21.16 | 0.13 | 6.38 | 1.60 | 20.19 | 18.48 | 0.15 | 2.40 | 20.17 |
| Perche | 0.08 | 1.87 | 34.06 | 28.78 | 15.90 | 6.85 | 0.96 | 3.17 | 0.69 | 7.64 |
| Javelot | 2.35 | 5.78 | 10.81 | 48.00 | 13.60 | 2.66 | 1.14 | 8.77 | 6.12 | 0.77 |
| 1500m | 0.10 | 12.95 | 43.54 | 2.46 | 3.44 | 8.90 | 0.70 | 0.02 | 9.47 | 18.42 |
Pour chacune des cinq premières composantes, regardons les variables qui y contribuent le plus.
for col in contributions.columns[0:5]:
df = contributions.loc[:,col].sort_values(ascending=False).reset_index()
fig, ax = plt.subplots(figsize=(8,2))
sns.barplot(data=df, x='index', y=col, color="tab:blue")
plt.title("Contributions des variables à " +col)
plt.xlabel("Variable")
plt.ylabel("Contribution (\%)")
plt.show()
Il est difficile de nommer les cinq premières composantes. Des disciplines très différentes contribuent fortement à chacune d'entre-elles.
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score
dico_inert = dict()
dico_silh = dict()
data = features_trans.iloc[:,0:5] #uniquement les 4 premières PC
for k in range(1,11):
kmeans = KMeans(n_clusters=k, n_init=100)
kmeans.fit(data)
clusters = kmeans.predict(data)
dico_inert[k] = kmeans.inertia_
if k>1:
dico_silh[k] = silhouette_score(data, clusters)
sq_sum = pd.DataFrame(list(dico_inert.items()), columns=['n_clusters', 'inertie'])
sq_sum
| n_clusters | inertie | |
|---|---|---|
| 0 | 1 | 334.378662 |
| 1 | 2 | 253.464351 |
| 2 | 3 | 205.075709 |
| 3 | 4 | 175.858653 |
| 4 | 5 | 149.589562 |
| 5 | 6 | 127.511105 |
| 6 | 7 | 114.867138 |
| 7 | 8 | 102.721672 |
| 8 | 9 | 93.350658 |
| 9 | 10 | 82.488584 |
# Représentation de la variance intra-clusters suivant le nombre de clusters
fig, ax = plt.subplots(figsize=(8,5))
plt.plot(sq_sum['n_clusters'], sq_sum['inertie'], color='tab:blue', marker='x')
plt.xticks(range(1,11), range(1,11))
plt.xlabel('Nombre de clusters')
plt.ylabel('Somme des carrés intra-cluster')
plt.title('Somme des carrés intra-cluster en fonction du nombre de clusters')
plt.tight_layout()
plt.show()
Il n'y a pas de coude franc.
df_sil = pd.DataFrame(list(dico_silh.items()), columns=['n_clusters', 'silhouette'])
df_sil
| n_clusters | silhouette | |
|---|---|---|
| 0 | 2 | 0.200473 |
| 1 | 3 | 0.185363 |
| 2 | 4 | 0.198417 |
| 3 | 5 | 0.222477 |
| 4 | 6 | 0.247857 |
| 5 | 7 | 0.231854 |
| 6 | 8 | 0.230253 |
| 7 | 9 | 0.226142 |
| 8 | 10 | 0.234294 |
fig, ax = plt.subplots(figsize=(10,5))
sns.lineplot(data=df_sil, x='n_clusters', y='silhouette')
sns.scatterplot(data=df_sil, x='n_clusters', y='silhouette')
plt.title('Score de silhouette en fonction du nombre de clusters')
plt.xlabel('Nombre de clusters')
plt.ylabel('Score de silhouette')
plt.show()
On peut choisir 6 clusters (meilleur score de silhouette).
kmeans = KMeans(n_clusters=6, n_init='auto')
kmeans.fit(data)
clusters = kmeans.predict(data)
df_clusters = pd.concat([data, pd.DataFrame({'Cluster':clusters})], axis=1)
df_clusters.head()
| PC1 | PC2 | PC3 | PC4 | PC5 | Cluster | |
|---|---|---|---|---|---|---|
| 0 | 4.038449 | 1.365826 | -0.289957 | 1.941134 | -0.376955 | 4 |
| 1 | 3.919365 | 0.836961 | 0.231175 | 1.493972 | 1.037609 | 4 |
| 2 | 4.619987 | 0.039995 | -0.041586 | -1.313526 | -0.187730 | 4 |
| 3 | 2.233461 | 1.041766 | -1.864362 | -0.743214 | -0.977270 | 4 |
| 4 | 2.168396 | -1.803200 | 0.851017 | -0.284600 | 0.151395 | 0 |
# Représentation des individus suivant PC1 et PC3
fig, ax = plt.subplots(figsize=(7,7))
plt.axhline(y=0, color='black', linewidth=1)
plt.axvline(x=0, color='black', linewidth=1)
sns.scatterplot(x='PC1', y='PC2', data=df_clusters, hue='Cluster', palette='tab10')
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
plt.xlim(-5,5)
plt.ylim(-5,5)
#deuxième repère pour les vecteurs
ax2 = fig.add_axes(ax.get_position(), frame_on=False)
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.xaxis.set_label_position('top')
ax2.yaxis.set_label_position('right')
ax2.set_xlabel('Variables initiales selon PC1', color='darkorange')
ax2.set_ylabel('Variables initiales selon PC2', color='darkorange')
ax2.tick_params(axis='both', labelcolor='darkorange')
ax2.set_xlim(-1,1)
ax2.set_ylim(-1,1)
ax2.set_xticks([-1, -0.5, 0, 0.5, 1])
ax2.set_yticks([-1, -0.5, 0, 0.5, 1])
# Représentation des vecteurs
for i in range(saturations.shape[0]):
x = saturations.iloc[i,0] # coord de la var i selon PC1
y = saturations.iloc[i,1] # coord de la var i selon PC2
feature = saturations.index[i] # nom de la var i
ax2.arrow(0,0, #départ flèche
x,y, #fin flèche
head_width=0.025,
head_length=0.025,
length_includes_head=True,
color='darkorange'
)
if feature!='Poids':
if x>0:
plt.text(x+0.03, y+0.05, feature)
else:
plt.text(x-0.15, y+0.05, feature)
else :
plt.text(x+0.05,y-0.05, feature)
# Titre
plt.title("Biplot : athlètes et disciplines suivant les deux premières composantes de l'ACP")
ax.legend(title='Cluster', bbox_to_anchor=[1.35,1])
# Grille
ax.grid(visible=True)
ax.set_axisbelow(True) # grille en arrière-plan
plt.show()
Sur le plan (PC1, PC2), les clusters ne sont pas tous clairement séparés. Mais n'oublions pas que les clustering n'a pas été fait que sur les deux première composantes principales.
from scipy.cluster.hierarchy import linkage, dendrogram
mergings = linkage(data, method='complete')
noms = df_decathlon['Nom'].to_list()
fig, ax = plt.subplots(figsize=(8,6))
dendrogram(mergings, labels=noms , leaf_rotation=90, leaf_font_size=7)
plt.axhline(4.5, color='black', linestyle='--')
plt.show()
D'après le dendrogramme, le choix de 6 clusters paraît être la meilleure solution. La ligne noire en pointillés indique à quel endroit se fait le "découpage". On remarque que les distances entre les différents groupes est assez grande à cette hauteur, ce qui n'était pas le cas à une hauteur plus faible. Cela indique que les clusters formés sont assez différents les uns des autres.
Une hautre méthode est de prendre une hauteur égale à 75 % de la hauteur maximale pour faire le découpage en clusters.
seuil = 0.75 * max(mergings[:, 2])
fig, ax = plt.subplots(figsize=(8,6))
dendrogram(mergings, labels=noms , leaf_rotation=90, leaf_font_size=7)
plt.axhline(seuil, color='black', linestyle='--')
plt.show()
Cette méthode suggère de ne conserver que 3 clusters. Dans la suite, je garderai 6 clusters.
from scipy.cluster.hierarchy import fcluster
clusters_cah = fcluster(mergings, t=4.5, criterion='distance')
df_clusters_cah = pd.concat([data, pd.DataFrame({'Cluster':clusters_cah})], axis=1)
df_clusters_cah.head()
| PC1 | PC2 | PC3 | PC4 | PC5 | Cluster | |
|---|---|---|---|---|---|---|
| 0 | 4.038449 | 1.365826 | -0.289957 | 1.941134 | -0.376955 | 5 |
| 1 | 3.919365 | 0.836961 | 0.231175 | 1.493972 | 1.037609 | 5 |
| 2 | 4.619987 | 0.039995 | -0.041586 | -1.313526 | -0.187730 | 5 |
| 3 | 2.233461 | 1.041766 | -1.864362 | -0.743214 | -0.977270 | 6 |
| 4 | 2.168396 | -1.803200 | 0.851017 | -0.284600 | 0.151395 | 3 |
# Représentation des individus suivant PC1 et PC3
fig, ax = plt.subplots(figsize=(7,7))
plt.axhline(y=0, color='black', linewidth=1)
plt.axvline(x=0, color='black', linewidth=1)
sns.scatterplot(x='PC1', y='PC2', data=df_clusters_cah, hue='Cluster', palette='tab10')
ax.set_xlabel('PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' \%)')
ax.set_ylabel('PC2 ('+ str(round(pca.explained_variance_ratio_[2]*100,2)) + ' \%)')
plt.xlim(-5,5)
plt.ylim(-5,5)
#deuxième repère pour les vecteurs
ax2 = fig.add_axes(ax.get_position(), frame_on=False)
ax2.xaxis.tick_top()
ax2.yaxis.tick_right()
ax2.xaxis.set_label_position('top')
ax2.yaxis.set_label_position('right')
ax2.set_xlabel('Variables initiales selon PC1', color='darkorange')
ax2.set_ylabel('Variables initiales selon PC2', color='darkorange')
ax2.tick_params(axis='both', labelcolor='darkorange')
ax2.set_xlim(-1,1)
ax2.set_ylim(-1,1)
ax2.set_xticks([-1, -0.5, 0, 0.5, 1])
ax2.set_yticks([-1, -0.5, 0, 0.5, 1])
# Représentation des vecteurs
for i in range(saturations.shape[0]):
x = saturations.iloc[i,0] # coord de la var i selon PC1
y = saturations.iloc[i,1] # coord de la var i selon PC2
feature = saturations.index[i] # nom de la var i
ax2.arrow(0,0, #départ flèche
x,y, #fin flèche
head_width=0.025,
head_length=0.025,
length_includes_head=True,
color='darkorange'
)
if feature!='Poids':
if x>0:
plt.text(x+0.03, y+0.05, feature)
else:
plt.text(x-0.15, y+0.05, feature)
else :
plt.text(x+0.05,y-0.05, feature)
# Titre
plt.title("Biplot : athlètes et disciplines suivant les deux premières composantes de l'ACP")
ax.legend(title='Cluster', bbox_to_anchor=[1.35,1])
# Grille
ax.grid(visible=True)
ax.set_axisbelow(True) # grille en arrière-plan
plt.show()
import plotly.graph_objects as go
fig = go.Figure()
# ensemble des clusters
unique_clusters = df_clusters_cah['Cluster'].sort_values().unique()
# Couleurs des clusters
colors = ['#053BA9', '#0A8D02', '#F7B200', '#CD0808', '#7810CE', '#10B7CE']
# couleur des points
conditions = [df_clusters_cah['Cluster'] ==1, df_clusters_cah['Cluster'] ==2, df_clusters_cah['Cluster'] ==3,
df_clusters_cah['Cluster'] ==4, df_clusters_cah['Cluster'] ==5, df_clusters_cah['Cluster'] ==6]
df_clusters_cah['couleur'] =np.select(conditions, colors)
# dico des coordonnées des points à placer (
# au départ, individus du jeu de données
points = {i:(df_clusters_cah.iloc[i,0], df_clusters_cah.iloc[i,1], 0, df_clusters_cah.iloc[i,6]) for i in range(41)}
points
# Sélection et représentation des individus de chaque cluster
for i, cluster in enumerate(unique_clusters):
df = df_clusters_cah[df_clusters_cah['Cluster'] == cluster]
fig.add_trace(go.Scatter3d(x=df['PC1'], y=df['PC2'], z=[0 for i in range(len(df))],
mode='markers',
marker=dict(size=6,
color=colors[i], # définir la couleur des points en fonction de la variable colors
opacity=1,
line=dict(width=1,
color='DarkSlateGrey')),
name=f'{cluster}'
)
)
der_pt = 40 #index du dernier point
# Pour chaque regroupement de mergings
for group in mergings:
pt1 = group[0]
pt2 = group[1]
ht = group[2]
#calcul des coordonnées du point de jonction et incrémentation de der_pt
der_pt = der_pt+1
x = (points[pt1][0] + points[pt2][0])/2
y = (points[pt1][1] + points[pt2][1])/2
# si les deux points d'origine ont même couleur
# alors le point et les lignes de jonction ont la même couleur
# sinon, ils sont noir
if points[pt1][3] == points[pt2][3]:
c = points[pt1][3]
else:
c = 'black'
#on place le point de jonction dans liste des points
points[der_pt] = (x, y, ht, c)
#lignes de regroupement
# Si les points d'origine ont même couleur, alors les lignes ont même couleur
# 1ere ligne vert du regroupement
fig.add_trace(go.Scatter3d(x=[points[pt1][0],points[pt1][0]], y=[points[pt1][1],points[pt1][1]], z=[points[pt1][2],ht],
mode='lines',
line=dict(color=c,
width=2),
showlegend=False
)
)
# 2eme ligne vert du regroupement
fig.add_trace(go.Scatter3d(x=[points[pt2][0],points[pt2][0]], y=[points[pt2][1],points[pt2][1]], z=[points[pt2][2],ht],
mode='lines',
line=dict(color=c,
width=2),
showlegend=False
)
)
# ligne horiz du regroupement
fig.add_trace(go.Scatter3d(x=[points[pt1][0],points[pt2][0]], y=[points[pt1][1],points[pt2][1]], z=[ht,ht],
mode='lines',
line=dict(color=c,
width=2),
showlegend=False
)
)
fig.update_layout(width=1000,
height=800,
scene=dict(xaxis=dict(showbackground=False, gridcolor='rgba(100, 150, 255, 0.8)', zerolinecolor='rgba(0, 0, 0, 1)'),
yaxis=dict(showbackground=False, gridcolor='rgba(100, 150, 255, 0.8)', zerolinecolor='rgba(0, 0, 0, 1)'),
zaxis=dict(showbackground=False, gridcolor='rgba(100, 150, 255, 0.8)', zerolinecolor='rgba(0, 0, 0, 1)',range=[0, max(mergings[:,2])]),
xaxis_title='PC1 ('+ str(round(pca.explained_variance_ratio_[0]*100,2)) + ' %)',
yaxis_title='PC2 ('+ str(round(pca.explained_variance_ratio_[1]*100,2)) + ' %)',
zaxis_title='Distance'),
legend=dict(title='Cluster'),
title='Classification Ascendante Hiérarchique des athlètes de décathlon'
)
fig.show()